【LIFF】アプリ内で会員登録したユーザーと未会員ユーザーを識別する方法を考えてみた

【LIFF】アプリ内で会員登録したユーザーと未会員ユーザーを識別する方法を考えてみた

Clock Icon2024.10.22

リテールアプリ共創部のるおんです。
LIFFアプリの開発において、アプリ内で会員登録したユーザーと未会員ユーザーを識別する必要性に直面しました。
これまでは会員登録したユーザーのLINE UIDを取得し、会員向けにメッセージ配信を行なったり会員データを用いて分析をしたりしていました。
しかし、これだと会員登録をしてくれなかったユーザーに対してメッセージ配信などのアクションを実行することができませんでした。今回はこの課題に対する解決策を考案し実装してみましたので、その方法をご紹介します。

やりたいこと

これまでの実装では、会員登録をしたユーザーのLINE UIDのみをデータベースに保存していました。しかし、これだと会員登録をしなかったユーザーの情報を取得できていません。そのため、以下のような要求に応えることができませんでした。

  • 公式アカウントを友達追加しLIFFアプリにアクセスしたが、会員登録まで至らなかったユーザーに対しても何らかのアクションを起こしたい。
  • 会員登録での離脱率を知りたい

イメージとしては以下のように、黄色の矢印で表現した未会員層を識別できるようにすることです。これまでは、青色の層のみをDBの会員テーブルに保存していたので、未会員ユーザーの情報を取得できていませんでした。

スクリーンショット 2024-10-22 15.53.15

これらの課題を解決するために、以下のアプローチを採用しました。
会員テーブルとは別に、一度でもアクセスしたユーザー全員を保存する専用テーブル(Guestテーブル)を作成し、そのテーブルにLINE UIDや名前の情報を格納するのに加えて、会員か未会員かのステータスを持たせるようにしました。
イメージとしては、以下の図のようなものです。

スクリーンショット 2024-10-22 16.14.52

このようにすることで、GuestテーブルのisMemberステータスがfalseのユーザー群のLINE UIDを使用することで、未会員ユーザーのみに特定アクション(メッセージ一斉配信など)をすることができます。

実際にやってみた

ではこの構成を実際に実装してみました。フロントエンドにReact、バックエンドにAWS LambdaをNode.jsを使って実装します。データベースはAmazon DynamoDBを用いています。

以下は簡単な手順です。

  1. 専用テーブル作成
    すべてのLIFFアプリアクセスユーザーの情報を保存するテーブル(Guestテーブル)を新設します。
  2. 初回アクセス時の情報送信(フロントエンド)
    アプリの初期表示画面にて、ユーザー情報を取得するためのアクセストークンをバックエンドに送信します。
  3. ユーザー情報の保存(バックエンド)
    受け取ったアクセストークンからユーザー情報を取得し、Guestテーブルに保存する。
    会員登録をする際にisMemberステータスを更新。

インフラストラクチャはAWS CDKを用いてサクッと作ってみました。

CDK

linUserIdをパーティションキーとするDynamoDBのGuestテーブルと、保存処理をするためのLambda、エンドポイントとしてのAPI Gatewayを作成しています。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as nodejs from 'aws-cdk-lib/aws-lambda-nodejs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as aws_dynamodb from 'aws-cdk-lib/aws-dynamodb';
import { RemovalPolicy } from 'aws-cdk-lib';

export class GuestLiffTestStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    /**
     * GuestUserを登録するLambda
     */
    const registerGuestUserFn = new nodejs.NodejsFunction(this, "registerGuestUserFn", {
      entry: "server/handler/registerGuestUserHandler.ts",
      runtime: lambda.Runtime.NODEJS_20_X,
      functionName: "registerGuestUserFn",
      description: "アクセスしたユーザーをすべて保存するLambda関数",
      architecture: lambda.Architecture.ARM_64,
    });

    /**
     * ゲスト用のAPI Gateway
     */
    const api = new apigateway.LambdaRestApi(this, "registerGuestUserApi", {
      handler: registerGuestUserFn,
      proxy: false,
      defaultCorsPreflightOptions: {
        allowOrigins: apigateway.Cors.ALL_ORIGINS,
        allowMethods: apigateway.Cors.ALL_METHODS,
        allowHeaders: apigateway.Cors.DEFAULT_HEADERS,
        statusCode: 200,
      },

    });

    const registerGuestUserIntegration = new apigateway.LambdaIntegration(registerGuestUserFn);
    api.root.addMethod("POST", registerGuestUserIntegration)

    /**
     * ゲスト用のDynamoDB
     */
    const guestTable = new aws_dynamodb.Table(
      this,
      "guestTable",
      {
        partitionKey: {
          name: "lineUserId",
          type: aws_dynamodb.AttributeType.STRING,
        },
        billingMode: aws_dynamodb.BillingMode.PAY_PER_REQUEST,
        removalPolicy: RemovalPolicy.RETAIN,
        tableName: "Guest",
        pointInTimeRecovery: true,
      },
    );

    guestTable.grantReadWriteData(registerGuestUserFn);
  }
}

フロントエンド

ユーザーが最初にアクセスするのが以下のような画面だとします。

IMG_0902

この画面を開いた瞬間、useEffectを使用してアクセストークンをバックエンドに送信し、ゲストユーザーを保存するエンドポイントを叩きます。

App.tsx
import { useEffect, useState } from "react";
import liff from "@line/liff";
import "./App.css";
import axios from "axios";

function App() {
// 省略
  const registerGuestUser = async () => {
    const accessToken = await liff.getAccessToken();
    if (accessToken) {
      const res = await axios.post(
        "https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod", // サーバーサイドへのリクエストエンドポイント
        { accessToken }
      );
    }
  };
+ // アクセス初回時に実行してあげる
+  useEffect(() => {
+    registerGuestUser();
+  }, []);

  return (
    <div className="App">
      <h1>アプリへようこそ!</h1>
      <a href="https://xxxxxxxx/">
        会員登録はこちらから
      </a>
    </div>
  );
}

export default App;

ちなみに、フロント側でユーザー情報を取得してバックエンドに送信するのではなく、必ずアクセストークンやIDトークンなどを送信するようにしてください。詳しくはこちらの記事で解説しています。
https://dev.classmethod.jp/articles/liff-line-user-profile-in-server-side/

バックエンド

次に、バックエンドを実装します。フロントから送られてきたアクセストークンを使用してユーザー情報を取得し、isMemberステータスをfalseとしてデータベースに保存します。

以下はLambda関数の例です。

import axios from "axios";
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand, GetCommand } from '@aws-sdk/lib-dynamodb';

// DynamoDBのクライアントの初期化
const dynamoDB = new DynamoDBClient({ region: 'ap-northeast-1' });
const dynamoDBDocumentClient = DynamoDBDocumentClient.from(dynamoDB);

export const handler = async (event: any) => {
  const { accessToken } = JSON.parse(event.body);

  // LINEユーザー情報を取得
  const userInfoResponse = await axios.get("https://api.line.me/v2/profile", {
    headers: {
      Authorization: `Bearer ${accessToken}`
    }
  });

  const lineUserId = userInfoResponse.data.userId;

  // DynamoDBからユーザーを取得
  const guestUser = await dynamoDBDocumentClient.send(new GetCommand({
    TableName: 'Guest',
    Key: {
      lineUserId: lineUserId
    }
  }));

  if (!guestUser.Item) {
    // ゲストが存在しない場合、新規作成  
+   await dynamoDBDocumentClient.send(new PutCommand({
+     TableName: 'Guest',
+     Item: {
+       lineUserId: lineUserId,
+       lineDisplayName: userInfoResponse.data.displayName,
+       pictureUrl: userInfoResponse.data.pictureUrl,
+       isMember: false // ステータスをfalseにして、未会員ユーザーとして識別できるようにする
+     }
    }));

    return {
      statusCode: 201,
      body: JSON.stringify({ message: "新しいユーザー情報を保存しました" })
    };
  } else {
    // ユーザーが既に存在する場合
    return {
      statusCode: 200,
      body: JSON.stringify({ message: "ユーザーは既に登録されています" })
    };
  }
};

実際にこのアプリにアクセスしてみると、ユーザー情報が保存されていることが確認できます。
スクリーンショット 2024-10-22 18.12.42
これで未会員のユーザー情報を取得できるようになりました。

では、次に会員登録をした際にはこのis_memberステータスをtrueにして、会員登録済みかどうかをわかるようにします。今回は実装していませんが、すでに会員登録処理がある前提で進めます。

// 会員情報の登録の際に、Guestテーブルの該当ユーザーのisMemberステータスを更新する
  await dynamoDBDocumentClient.send(new UpdateCommand({
    TableName: 'Guest',
    Key: {
      lineUserId: userInfoResponse.data.userId
    },
    UpdateExpression: 'SET isMember = :isMember',
    ExpressionAttributeValues: {
+     ':isMember': true
    },
    ReturnValues: 'UPDATED_NEW'
  }));

  // 会員情報を登録する処理
  // 省略

  return {
    statusCode: 200,
    body: JSON.stringify({ message: "会員情報を保存しました" })
  };
};

会員登録をすると同時に、GuestテーブルのisMemberステータスがtrueになっていることが確認できます。
スクリーンショット 2024-10-22 18.30.01

このようにすることで、利用ユーザー数が増えた場合に、ユーザーが未会員ユーザーなのか、会員登録済みユーザーなのかを識別することができますね。

スクリーンショット 2024-10-22 18.40.18

そして、未会員ユーザーのみのLINE UIDを用いてオーディエンスを作成し、メッセージを配信するなどのアクションをすることが可能になります。

おわりに

今回、LIFFアプリにおける会員・未会員ユーザーの識別方法について、具体的な実装例を交えて紹介しました。この手法を用いることで、以下のような利点が得られます:

  • アプリにアクセスしたすべてのユーザーの情報を取得し、包括的なユーザー分析が可能になる。
  • 未会員ユーザーに特化したメッセージ配信や施策を実施できるようになり、会員登録率の向上につながる可能性がある。
  • アプリアクセスから会員登録までの過程を追跡できるため、離脱率や転換率などの重要なメトリクスを測定できる。

また、今回はisMemberフラグで会員登録済みかどうかを判断しましたが、単純にGuestテーブルと会員テーブルを照らし合わせるだけでも未会員ユーザーかどうかを判断することは可能です。自身のアプリケーションに応じてこの辺りは柔軟に構成してください。

以上、どなたかの参考になれば幸いです。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.